/**
 * @license
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

// Navigation parameters object format:
//
// Each object has a `view` property with a value from GerritNav.View. The
// remaining properties depend on the value used for view.
//
//  - GerritNav.View.CHANGE:
//    - `changeNum`, required, String: the numeric ID of the change.
//    - `project`, optional, String: the project name.
//    - `patchNum`, optional, Number: the patch for the right-hand-side of
//        the diff.
//    - `basePatchNum`, optional, Number: the patch for the left-hand-side
//        of the diff. If `basePatchNum` is provided, then `patchNum` must
//        also be provided.
//    - `edit`, optional, Boolean: whether or not to load the file list with
//        edit controls.
//    - `messageHash`, optional, String: the hash of the change message to
//        scroll to.
//
// - GerritNav.View.SEARCH:
//    - `query`, optional, String: the literal search query. If provided,
//        the string will be used as the query, and all other params will be
//        ignored.
//    - `owner`, optional, String: the owner name.
//    - `project`, optional, String: the project name.
//    - `branch`, optional, String: the branch name.
//    - `topic`, optional, String: the topic name.
//    - `hashtag`, optional, String: the hashtag name.
//    - `statuses`, optional, Array<String>: the list of change statuses to
//        search for. If more than one is provided, the search will OR them
//        together.
//    - `offset`, optional, Number: the offset for the query.
//
//  - GerritNav.View.DIFF:
//    - `changeNum`, required, String: the numeric ID of the change.
//    - `path`, required, String: the filepath of the diff.
//    - `patchNum`, required, Number: the patch for the right-hand-side of
//        the diff.
//    - `basePatchNum`, optional, Number: the patch for the left-hand-side
//        of the diff. If `basePatchNum` is provided, then `patchNum` must
//        also be provided.
//    - `lineNum`, optional, Number: the line number to be selected on load.
//    - `leftSide`, optional, Boolean: if a `lineNum` is provided, a value
//        of true selects the line from base of the patch range. False by
//        default.
//
//  - GerritNav.View.GROUP:
//    - `groupId`, required, String: the ID of the group.
//    - `detail`, optional, String: the name of the group detail view.
//      Takes any value from GerritNav.GroupDetailView.
//
//  - GerritNav.View.REPO:
//    - `repoName`, required, String: the name of the repo
//    - `detail`, optional, String: the name of the repo detail view.
//      Takes any value from GerritNav.RepoDetailView.
//
//  - GerritNav.View.DASHBOARD
//    - `repo`, optional, String.
//    - `sections`, optional, Array of objects with `title` and `query`
//      strings.
//    - `user`, optional, String.
//
//  - GerritNav.View.ROOT:
//    - no possible parameters.

const uninitialized = () => {
  console.warn('Use of uninitialized routing');
};

const EDIT_PATCHNUM = 'edit';
const PARENT_PATCHNUM = 'PARENT';

const USER_PLACEHOLDER_PATTERN = /\${user}/g;

// NOTE: These queries are tested in Java. Any changes made to definitions
// here require corresponding changes to:
// javatests/com/google/gerrit/server/query/change/AbstractQueryChangesTest.java
const DEFAULT_SECTIONS = [
  {
    // Changes with unpublished draft comments. This section is omitted when
    // viewing other users, so we don't need to filter anything out.
    name: 'Has draft comments',
    query: 'has:draft',
    selfOnly: true,
    hideIfEmpty: true,
    suffixForDashboard: 'limit:10',
  },
  {
    // Changes where the user is in the attention set.
    name: 'Your Turn',
    query: 'attention:${user}',
    hideIfEmpty: false,
    suffixForDashboard: 'limit:25',
    attentionSetOnly: true,
  },
  {
    // Changes that are assigned to the viewed user.
    name: 'Assigned reviews',
    query: 'assignee:${user} (-is:wip OR owner:self OR assignee:self) ' +
        'is:open -is:ignored',
    hideIfEmpty: true,
    suffixForDashboard: 'limit:25',
    assigneeOnly: true,
  },
  {
    // WIP open changes owned by viewing user. This section is omitted when
    // viewing other users, so we don't need to filter anything out.
    name: 'Work in progress',
    query: 'is:open owner:${user} is:wip',
    selfOnly: true,
    hideIfEmpty: true,
    suffixForDashboard: 'limit:25',
  },
  {
    // Non-WIP open changes owned by viewed user. Filter out changes ignored
    // by the viewing user.
    name: 'Outgoing reviews',
    query: 'is:open owner:${user} -is:wip -is:ignored',
    isOutgoing: true,
    suffixForDashboard: 'limit:25',
  },
  {
    // Non-WIP open changes not owned by the viewed user, that the viewed user
    // is associated with (as either a reviewer or the assignee). Changes
    // ignored by the viewing user are filtered out.
    name: 'Incoming reviews',
    query: 'is:open -owner:${user} -is:wip -is:ignored ' +
        '(reviewer:${user} OR assignee:${user})',
    suffixForDashboard: 'limit:25',
  },
  {
    // Open changes the viewed user is CCed on. Changes ignored by the viewing
    // user are filtered out.
    name: 'CCed on',
    query: 'is:open -is:ignored cc:${user}',
    suffixForDashboard: 'limit:10',
  },
  {
    name: 'Recently closed',
    // Closed changes where viewed user is owner, reviewer, or assignee.
    // Changes ignored by the viewing user are filtered out, and so are WIP
    // changes not owned by the viewing user (the one instance of
    // 'owner:self' is intentional and implements this logic).
    query: 'is:closed -is:ignored (-is:wip OR owner:self) ' +
        '(owner:${user} OR reviewer:${user} OR assignee:${user} ' +
        'OR cc:${user})',
    suffixForDashboard: '-age:4w limit:10',
  },
];

// TODO(dmfilippov) Convert to class, extract consts, give better name and
// expose as a service from appContext
export const GerritNav = {

  View: {
    ADMIN: 'admin',
    AGREEMENTS: 'agreements',
    CHANGE: 'change',
    DASHBOARD: 'dashboard',
    DIFF: 'diff',
    DOCUMENTATION_SEARCH: 'documentation-search',
    EDIT: 'edit',
    GROUP: 'group',
    PLUGIN_SCREEN: 'plugin-screen',
    REPO: 'repo',
    ROOT: 'root',
    SEARCH: 'search',
    SETTINGS: 'settings',
  },

  GroupDetailView: {
    MEMBERS: 'members',
    LOG: 'log',
  },

  RepoDetailView: {
    ACCESS: 'access',
    BRANCHES: 'branches',
    COMMANDS: 'commands',
    DASHBOARDS: 'dashboards',
    TAGS: 'tags',
  },

  WeblinkType: {
    CHANGE: 'change',
    FILE: 'file',
    PATCHSET: 'patchset',
  },

  /** @type {Function} */
  _navigate: uninitialized,

  /** @type {Function} */
  _generateUrl: uninitialized,

  /** @type {Function} */
  _generateWeblinks: uninitialized,

  /** @type {Function} */
  mapCommentlinks: uninitialized,

  /**
   * @param {number=} patchNum
   * @param {number|string=} basePatchNum
   */
  _checkPatchRange(patchNum, basePatchNum) {
    if (basePatchNum && !patchNum) {
      throw new Error('Cannot use base patch number without patch number.');
    }
  },

  /**
   * Setup router implementation.
   *
   * @param {function(!string)} navigate the router-abstracted equivalent of
   *     `window.location.href = ...`. Takes a string.
   * @param {function(!Object): string} generateUrl generates a URL given
   *     navigation parameters, detailed in the file header.
   * @param {function(!Object): string} generateWeblinks weblinks generator
   *     function takes single payload parameter with type property that
   *  determines which
   *     part of the UI is the consumer of the weblinks. type property can
   *     be one of file, change, or patchset.
   *     - For file type, payload will also contain string properties: repo,
   *         commit, file.
   *     - For patchset type, payload will also contain string properties:
   *         repo, commit.
   *     - For change type, payload will also contain string properties:
   *         repo, commit. If server provides weblinks, those will be passed
   *         as options.weblinks property on the main payload object.
   * @param {function(!Object): Object} mapCommentlinks provides an escape
   *     hatch to modify the commentlinks object, e.g. if it contains any
   *     relative URLs.
   */
  setup(navigate, generateUrl, generateWeblinks, mapCommentlinks) {
    this._navigate = navigate;
    this._generateUrl = generateUrl;
    this._generateWeblinks = generateWeblinks;
    this.mapCommentlinks = mapCommentlinks;
  },

  destroy() {
    this._navigate = uninitialized;
    this._generateUrl = uninitialized;
    this._generateWeblinks = uninitialized;
    this.mapCommentlinks = uninitialized;
  },

  /**
   * Generate a URL for the given route parameters.
   *
   * @param {Object} params
   * @return {string}
   */
  _getUrlFor(params) {
    return this._generateUrl(params);
  },

  getUrlForSearchQuery(query, opt_offset) {
    return this._getUrlFor({
      view: GerritNav.View.SEARCH,
      query,
      offset: opt_offset,
    });
  },

  /**
   * @param {!string} project The name of the project.
   * @param {boolean=} opt_openOnly When true, only search open changes in
   *     the project.
   * @param {string=} opt_host The host in which to search.
   * @return {string}
   */
  getUrlForProjectChanges(project, opt_openOnly, opt_host) {
    return this._getUrlFor({
      view: GerritNav.View.SEARCH,
      project,
      statuses: opt_openOnly ? ['open'] : [],
      host: opt_host,
    });
  },

  /**
   * @param {string} branch The name of the branch.
   * @param {string} project The name of the project.
   * @param {string=} opt_status The status to search.
   * @param {string=} opt_host The host in which to search.
   * @return {string}
   */
  getUrlForBranch(branch, project, opt_status, opt_host) {
    return this._getUrlFor({
      view: GerritNav.View.SEARCH,
      branch,
      project,
      statuses: opt_status ? [opt_status] : undefined,
      host: opt_host,
    });
  },

  /**
   * @param {string} topic The name of the topic.
   * @param {string=} opt_host The host in which to search.
   * @return {string}
   */
  getUrlForTopic(topic, opt_host) {
    return this._getUrlFor({
      view: GerritNav.View.SEARCH,
      topic,
      statuses: ['open', 'merged'],
      host: opt_host,
    });
  },

  /**
   * @param {string} hashtag The name of the hashtag.
   * @return {string}
   */
  getUrlForHashtag(hashtag) {
    return this._getUrlFor({
      view: GerritNav.View.SEARCH,
      hashtag,
      statuses: ['open', 'merged'],
    });
  },

  /**
   * Navigate to a search for changes with the given status.
   *
   * @param {string} status
   */
  navigateToStatusSearch(status) {
    this._navigate(this._getUrlFor({
      view: GerritNav.View.SEARCH,
      statuses: [status],
    }));
  },

  /**
   * Navigate to a search query
   *
   * @param {string} query
   * @param {number=} opt_offset
   */
  navigateToSearchQuery(query, opt_offset) {
    return this._navigate(this.getUrlForSearchQuery(query, opt_offset));
  },

  /**
   * Navigate to the user's dashboard
   */
  navigateToUserDashboard() {
    return this._navigate(this.getUrlForUserDashboard('self'));
  },

  /**
   * @param {!Object} change The change object.
   * @param {number=} opt_patchNum
   * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
   *     used for none.
   * @param {boolean=} opt_isEdit
   * @param {string=} opt_messageHash
   * @return {string}
   */
  getUrlForChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit,
      opt_messageHash) {
    if (opt_basePatchNum === PARENT_PATCHNUM) {
      opt_basePatchNum = undefined;
    }

    this._checkPatchRange(opt_patchNum, opt_basePatchNum);
    return this._getUrlFor({
      view: GerritNav.View.CHANGE,
      changeNum: change._number,
      project: change.project,
      patchNum: opt_patchNum,
      basePatchNum: opt_basePatchNum,
      edit: opt_isEdit,
      host: change.internalHost || undefined,
      messageHash: opt_messageHash,
    });
  },

  /**
   * @param {number} changeNum
   * @param {string} project The name of the project.
   * @param {number=} opt_patchNum
   * @return {string}
   */
  getUrlForChangeById(changeNum, project, opt_patchNum) {
    return this._getUrlFor({
      view: GerritNav.View.CHANGE,
      changeNum,
      project,
      patchNum: opt_patchNum,
    });
  },

  /**
   * @param {!Object} change The change object.
   * @param {number=} opt_patchNum
   * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
   *     used for none.
   * @param {boolean=} opt_isEdit
   */
  navigateToChange(change, opt_patchNum, opt_basePatchNum, opt_isEdit) {
    this._navigate(this.getUrlForChange(change, opt_patchNum,
        opt_basePatchNum, opt_isEdit));
  },

  /**
   * @param {{ _number: number, project: string }} change The change object.
   * @param {string} path The file path.
   * @param {number=} opt_patchNum
   * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
   *     used for none.
   * @param {number|string=} opt_lineNum
   * @return {string}
   */
  getUrlForDiff(change, path, opt_patchNum, opt_basePatchNum, opt_lineNum) {
    return this.getUrlForDiffById(change._number, change.project, path,
        opt_patchNum, opt_basePatchNum, opt_lineNum);
  },

  /**
   * @param {number} changeNum
   * @param {string} project The name of the project.
   * @param {string} path The file path.
   * @param {number=} opt_patchNum
   * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
   *     used for none.
   * @param {number=} opt_lineNum
   * @param {boolean=} opt_leftSide
   * @return {string}
   */
  getUrlForDiffById(changeNum, project, path, opt_patchNum,
      opt_basePatchNum, opt_lineNum, opt_leftSide) {
    if (opt_basePatchNum === PARENT_PATCHNUM) {
      opt_basePatchNum = undefined;
    }

    this._checkPatchRange(opt_patchNum, opt_basePatchNum);
    return this._getUrlFor({
      view: GerritNav.View.DIFF,
      changeNum,
      project,
      path,
      patchNum: opt_patchNum,
      basePatchNum: opt_basePatchNum,
      lineNum: opt_lineNum,
      leftSide: opt_leftSide,
    });
  },

  /**
   * @param {{ _number: number, project: string }} change The change object.
   * @param {string} path The file path.
   * @param {number=} opt_patchNum
   * @param {number=} opt_lineNum
   * @return {string}
   */
  getEditUrlForDiff(change, path, opt_patchNum, opt_lineNum) {
    return this.getEditUrlForDiffById(change._number, change.project, path,
        opt_patchNum, opt_lineNum);
  },

  /**
   * @param {number} changeNum
   * @param {string} project The name of the project.
   * @param {string} path The file path.
   * @param {number|string=} opt_patchNum The patchNum the file content
   *    should be based on, or ${EDIT_PATCHNUM} if left undefined.
   * @param {number=} opt_lineNum The line number to pass to the inline editor.
   * @return {string}
   */
  getEditUrlForDiffById(changeNum, project, path, opt_patchNum, opt_lineNum) {
    return this._getUrlFor({
      view: GerritNav.View.EDIT,
      changeNum,
      project,
      path,
      patchNum: opt_patchNum || EDIT_PATCHNUM,
      lineNum: opt_lineNum,
    });
  },

  /**
   * @param {!Object} change The change object.
   * @param {string} path The file path.
   * @param {number=} opt_patchNum
   * @param {number|string=} opt_basePatchNum The string 'PARENT' can be
   *     used for none.
   */
  navigateToDiff(change, path, opt_patchNum, opt_basePatchNum) {
    this._navigate(this.getUrlForDiff(change, path, opt_patchNum,
        opt_basePatchNum));
  },

  /**
   * @param {string} owner The name of the owner.
   * @return {string}
   */
  getUrlForOwner(owner) {
    return this._getUrlFor({
      view: GerritNav.View.SEARCH,
      owner,
    });
  },

  /**
   * @param {string} user The name of the user.
   * @return {string}
   */
  getUrlForUserDashboard(user) {
    return this._getUrlFor({
      view: GerritNav.View.DASHBOARD,
      user,
    });
  },

  /**
   * @return {string}
   */
  getUrlForRoot() {
    return this._getUrlFor({
      view: GerritNav.View.ROOT,
    });
  },

  /**
   * @param {string} repo The name of the repo.
   * @param {string} dashboard The ID of the dashboard, in the form of
   *     '<ref>:<path>'.
   * @return {string}
   */
  getUrlForRepoDashboard(repo, dashboard) {
    return this._getUrlFor({
      view: GerritNav.View.DASHBOARD,
      repo,
      dashboard,
    });
  },

  /**
   * Navigate to an arbitrary relative URL.
   *
   * @param {string} relativeUrl
   */
  navigateToRelativeUrl(relativeUrl) {
    if (!relativeUrl.startsWith('/')) {
      throw new Error('navigateToRelativeUrl with non-relative URL');
    }
    this._navigate(relativeUrl);
  },

  /**
   * @param {string} repoName
   * @return {string}
   */
  getUrlForRepo(repoName) {
    return this._getUrlFor({
      view: GerritNav.View.REPO,
      repoName,
    });
  },

  /**
   * Navigate to a repo settings page.
   *
   * @param {string} repoName
   */
  navigateToRepo(repoName) {
    this._navigate(this.getUrlForRepo(repoName));
  },

  /**
   * @param {string} repoName
   * @return {string}
   */
  getUrlForRepoTags(repoName) {
    return this._getUrlFor({
      view: GerritNav.View.REPO,
      repoName,
      detail: GerritNav.RepoDetailView.TAGS,
    });
  },

  /**
   * @param {string} repoName
   * @return {string}
   */
  getUrlForRepoBranches(repoName) {
    return this._getUrlFor({
      view: GerritNav.View.REPO,
      repoName,
      detail: GerritNav.RepoDetailView.BRANCHES,
    });
  },

  /**
   * @param {string} repoName
   * @return {string}
   */
  getUrlForRepoAccess(repoName) {
    return this._getUrlFor({
      view: GerritNav.View.REPO,
      repoName,
      detail: GerritNav.RepoDetailView.ACCESS,
    });
  },

  /**
   * @param {string} repoName
   * @return {string}
   */
  getUrlForRepoCommands(repoName) {
    return this._getUrlFor({
      view: GerritNav.View.REPO,
      repoName,
      detail: GerritNav.RepoDetailView.COMMANDS,
    });
  },

  /**
   * @param {string} repoName
   * @return {string}
   */
  getUrlForRepoDashboards(repoName) {
    return this._getUrlFor({
      view: GerritNav.View.REPO,
      repoName,
      detail: GerritNav.RepoDetailView.DASHBOARDS,
    });
  },

  /**
   * @param {string} groupId
   * @return {string}
   */
  getUrlForGroup(groupId) {
    return this._getUrlFor({
      view: GerritNav.View.GROUP,
      groupId,
    });
  },

  /**
   * @param {string} groupId
   * @return {string}
   */
  getUrlForGroupLog(groupId) {
    return this._getUrlFor({
      view: GerritNav.View.GROUP,
      groupId,
      detail: GerritNav.GroupDetailView.LOG,
    });
  },

  /**
   * @param {string} groupId
   * @return {string}
   */
  getUrlForGroupMembers(groupId) {
    return this._getUrlFor({
      view: GerritNav.View.GROUP,
      groupId,
      detail: GerritNav.GroupDetailView.MEMBERS,
    });
  },

  getUrlForSettings() {
    return this._getUrlFor({view: GerritNav.View.SETTINGS});
  },

  /**
   * @param {string} repo
   * @param {string} commit
   * @param {string} file
   * @param {Object=} opt_options
   * @return {
   *   Array<{label: string, url: string}>|
   *   {label: string, url: string}
   *  }
   */
  getFileWebLinks(repo, commit, file, opt_options) {
    const params = {type: GerritNav.WeblinkType.FILE, repo, commit, file};
    if (opt_options) {
      params.options = opt_options;
    }
    return [].concat(this._generateWeblinks(params));
  },

  /**
   * @param {string} repo
   * @param {string} commit
   * @param {Object=} opt_options
   * @return {{label: string, url: string}}
   */
  getPatchSetWeblink(repo, commit, opt_options) {
    const params = {type: GerritNav.WeblinkType.PATCHSET, repo, commit};
    if (opt_options) {
      params.options = opt_options;
    }
    const result = this._generateWeblinks(params);
    if (Array.isArray(result)) {
      return result.pop();
    } else {
      return result;
    }
  },

  /**
   * @param {string} repo
   * @param {string} commit
   * @param {Object=} opt_options
   * @return {
   *   Array<{label: string, url: string}>|
   *   {label: string, url: string}
   *  }
   */
  getChangeWeblinks(repo, commit, opt_options) {
    const params = {type: GerritNav.WeblinkType.CHANGE, repo, commit};
    if (opt_options) {
      params.options = opt_options;
    }
    return [].concat(this._generateWeblinks(params));
  },

  getUserDashboard(user = 'self', sections = DEFAULT_SECTIONS,
      title = '', config = {}) {
    const attentionEnabled =
        config.change && !!config.change.enable_attention_set;
    const assigneeEnabled =
        config.change && !!config.change.enable_assignee;
    sections = sections
        .filter(section => (attentionEnabled || !section.attentionSetOnly))
        .filter(section => (assigneeEnabled || !section.assigneeOnly))
        .filter(section => (user === 'self' || !section.selfOnly))
        .map(section => Object.assign({}, section, {
          name: section.name,
          query: section.query.replace(USER_PLACEHOLDER_PATTERN, user),
        }));
    return {title, sections};
  },
};
